Node.jsでコネクションプールを使ってMySQLの暗黙的コミットを起こしてみた

Node.jsでコネクションプールを使ってMySQLの暗黙的コミットを起こしてみた

Clock Icon2024.10.30

こんにちは、ゲームソリューション部のsoraです。
今回は、コネクションプールを使ってMySQLの暗黙的コミットを起こしてみたことについて書いていきます。

MySQLの暗黙的コミットとは

公式ドキュメントは以下です。
https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html

暗黙的コミットについてまとめたブログは以下です。
https://dev.classmethod.jp/articles/mysql-implicit-commit/

暗黙的コミットの中で、今回関係のある部分は以下です。

  • トランザクション制御のステートメント
    • BEGIN、LOCK TABLES、SET autocommit = 1 (1でない状態から1に変えた場合)、START TRANSACTION、UNLOCK TABLESなどでは暗黙的コミットが入る
    • トランザクションのネストはできない

今回は、実際にAPIサーバを構築して、コネクションプールを使って暗黙的コミットを起こしてみます。
本ブログでは意図的にやっていますが、実際の開発時にもトランザクション処理でコミットやロールバックを忘れると、意図しない暗黙的コミットが発生してしまいます。

環境構築

ECS(Fargate)でAPIサーバ(Node.js)を構築し、Amazon Aurora(MySQL)へクエリを実行する構成にします。

APIサーバ

以下がAPIサーバのコードです。(言語はTypeScript)
意図的に暗黙的コミットを起こすため、コネクションプールを1にして2回のリクエストが同じコネクションを使用するようにし、try文の中でawait connection.commit();をあえて書き忘れた形にしています。

今回の検証に関係はありませんが、ロガーとしてpinoを使用してCloudWatch Logsにログを出力しています。
コネクションプールを使うパターンと使わないパターンの両方で実施するため、コメントアウトでコネクションプールを使わないパターンも記載しています。

index.ts
import express, { Request, Response } from 'express';
import mysql, { Pool, PoolConnection } from 'mysql2/promise';
// import mysql, { Connection } from 'mysql2/promise';
import 'dotenv/config';
import pino from 'pino';

// 標準出力
const logger = pino();

const app = express();

// データベース接続設定
const dbConfig = {
    host: process.env.HOST as string,
    port: parseInt(process.env.PORT as string, 10),
    user: process.env.USER as string,
    password: process.env.PASSWORD as string,
    database: process.env.DATABASE as string,
    connectionLimit: 1
};

// コネクションプールの作成
const pool: Pool = mysql.createPool(dbConfig);

// リクエストを受けるポート
const PORT = 3000;

// テストAPI
app.get('/implicit-test', async (req, res) => {
    let connection: PoolConnection | undefined;
    // let connection: Connection | undefined;
    const {prefecture, prefectural_capital} = req.query;

    try {
        connection = await pool.getConnection();
        // connection = await mysql.createConnection(dbConfig);
        logger.info('Pool get successfully');

        await connection.beginTransaction();
        logger.info('Transaction started');

        await connection.execute(
            `INSERT INTO prefectures (
                prefecture,
                prefectural_capital
            ) VALUES (?, ?)`,
            [prefecture, prefectural_capital]
        );

        // トランザクションの結果を返却
        res.status(200).json({
            message: 'Query OK',
            prefecture,
            prefectural_capital
        });

    } catch (err: any) {
        if (connection) {
            await connection.rollback();
        }
        res.status(500).json({ error: 'Database query failed' });
        logger.error({
            msg: 'Error executing query',
            errMessage: err.message,
            errStack: err.stack
        });

    } finally {
        if (connection) {
            connection.release();
            // connection.end();
        }
    }
});

app.use((req: Request, res: Response) => {
    logger.error('Not Found:', req.originalUrl);
    res.status(404).json({ message: 'Not Found' });
});

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

ECS上で実行するため、dockerfileも作成します。
その他、package.json.envファイルも必要ですが記載は割愛します。

dockerfile
FROM node:alpine

WORKDIR /app

RUN apk update

COPY /ts .
RUN npm install

CMD ["npm", "run", "start"]

DB

DBには、事前にデータベースとテーブルを作成しておきます。
検証用にMySQLクライアントがインストールされているEC2インスタンスがあったため、そちらを使用して事前準備をします。

CREATE DATABASE test;

USE test;

CREATE TABLE prefectures (
    id SERIAL PRIMARY KEY,
    prefecture VARCHAR(255) NOT NULL,
    prefectural_capital VARCHAR(255) NOT NULL
);

動作検証

検証の流れとして、まず1つ目のリクエストを実行した後、テーブル内のデータを確認します。
その後、2つ目のリクエストを実行した後に、同様にテーブル内のデータを確認します。

コネクションプールを使ったパターン

1つ目のリクエストを実行します。

http://{public ip}:3000/implicit-test?prefecture=Kanagawa&prefectural_capital=Yokohama
{"message":"Query OK","prefecture":"Kanagawa","prefectural_capital":"Yokohama"}

テーブル内のデータを確認してみると、トランザクション処理でコミットしていないため、データは挿入されていません。

mysql> select * from prefectures;
Empty set (0.00 sec)

次に2つ目のリクエストを実行します。
今回、コネクションプールは1にしているため、1つ目のリクエストと同じコネクションが使われることになります。

http://{public ip}:3000/implicit-test?prefecture=Hyogo&prefectural_capital=Kobe
{"message":"Query OK","prefecture":"Hyogo","prefectural_capital":"Kobe"}

テーブル内のデータを確認してみると、await connection.beginTransaction();で暗黙的コミットが発生し、1つ目のリクエストのデータがコミットされて挿入されています。
2つ目のリクエストのデータはコミットしていないため入っていません。

mysql> select * from prefectures;
+----+------------+---------------------+
| id | prefecture | prefectural_capital |
+----+------------+---------------------+
|  1 | Kanagawa   | Yokohama            |
+----+------------+---------------------+
1 row in set (0.00 sec)

コネクションプールを使わないパターン(都度コネクションを作成)

先ほどと同様に進めていきます。
まず1つ目のリクエストを実行します。

http://{public ip}:3000/implicit-test?prefecture=Kanagawa&prefectural_capital=Yokohama
{"message":"Query OK","prefecture":"Kanagawa","prefectural_capital":"Yokohama"}

テーブル内のデータを確認してみると、トランザクション処理でコミットしていないため、データは挿入されていません。

mysql> select * from prefectures;
Empty set (0.00 sec)

次に2つ目のリクエストを実行します。
コネクションプールを使用しておらず、都度コネクションを作成するため、1つ目のリクエストと使用するコネクションは異なります。

http://{public ip}:3000/implicit-test?prefecture=Hyogo&prefectural_capital=Kobe
{"message":"Query OK","prefecture":"Hyogo","prefectural_capital":"Kobe"}

テーブル内のデータを確認してみると、コネクションが別のため暗黙的コミットは発生していません。

mysql> select * from prefectures;
Empty set (0.00 sec)

最後に

今回は、コネクションプールを使ってMySQLの暗黙的コミットを起こしてみたことを記事にしました。
MySQLでトランザクション処理をする際は、コミットとロールバックの記載を忘れないようにしないと、意図せずコミットが入ってしまうため注意しましょう。

私はこれほど単純なコードではないものの、上記のような事象が発生したため、知見を残すためにブログにしました。
どなたかの参考になると幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.